/*
Copyright 2024 New Vector Ltd.
Copyright 2015 OpenMarket Ltd

SPDX-License-Identifier: AGPL-3.0-only
Please see LICENSE in the repository root for full details.
 */

#import "MXKSearchViewController.h"

#import "MXKSearchTableViewCell.h"

#import "NSBundle+MatrixKit.h"

#import "MXKSwiftHeader.h"

@interface MXKSearchViewController ()
{
    /**
     Optional bar buttons
     */
    UIBarButtonItem *searchBarButton;

    /**
     Search handling
     */
    BOOL ignoreSearchRequest;
}
@end

@implementation MXKSearchViewController
@synthesize dataSource, shouldScrollToBottomOnRefresh;

#pragma mark - Class methods

+ (UINib *)nib
{
    return [UINib nibWithNibName:NSStringFromClass([MXKSearchViewController class])
                          bundle:[NSBundle bundleForClass:[MXKSearchViewController class]]];
}

+ (instancetype)searchViewController
{
    return [[[self class] alloc] initWithNibName:NSStringFromClass([MXKSearchViewController class])
                                          bundle:[NSBundle bundleForClass:[MXKSearchViewController class]]];
}

#pragma mark -

- (void)finalizeInit
{
    [super finalizeInit];
    
    _enableBarButtonSearch = YES;
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    // Check whether the view controller has been pushed via storyboard
    if (!_searchTableView)
    {
        // Instantiate view controller objects
        [[[self class] nib] instantiateWithOwner:self options:nil];
    }

    // Adjust Top and Bottom constraints to take into account potential navBar and tabBar.
    [NSLayoutConstraint deactivateConstraints:@[_searchSearchBarTopConstraint, _searchTableViewBottomConstraint]];

    #pragma clang diagnostic push
    #pragma clang diagnostic ignored "-Wdeprecated"
    _searchSearchBarTopConstraint = [NSLayoutConstraint constraintWithItem:self.topLayoutGuide
                                                                  attribute:NSLayoutAttributeBottom
                                                                  relatedBy:NSLayoutRelationEqual
                                                                     toItem:self.searchSearchBar
                                                                  attribute:NSLayoutAttributeTop
                                                                 multiplier:1.0f
                                                                   constant:0.0f];

    _searchTableViewBottomConstraint = [NSLayoutConstraint constraintWithItem:self.bottomLayoutGuide
                                                                     attribute:NSLayoutAttributeTop
                                                                     relatedBy:NSLayoutRelationEqual
                                                                        toItem:self.searchTableView
                                                                     attribute:NSLayoutAttributeBottom
                                                                    multiplier:1.0f
                                                                      constant:0.0f];
    #pragma clang diagnostic pop

    [NSLayoutConstraint activateConstraints:@[_searchSearchBarTopConstraint, _searchTableViewBottomConstraint]];

    // Hide search bar by default
    self.searchSearchBar.hidden = YES;
    self.searchSearchBarHeightConstraint.constant = 0;
    [self.view setNeedsUpdateConstraints];

    self.noResultsLabel.text = [VectorL10n searchNoResults];
    self.noResultsLabel.hidden = YES;

    searchBarButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemSearch target:self action:@selector(showSearchBar:)];

    // Apply search option in navigation bar
    self.enableBarButtonSearch = _enableBarButtonSearch;

    // Finalize table view configuration
    _searchTableView.delegate = self;
    _searchTableView.dataSource = dataSource; // Note: dataSource may be nil here

    // Set up classes to use for cells
    [self.searchTableView registerNib:MXKSearchTableViewCell.nib forCellReuseIdentifier:MXKSearchTableViewCell.defaultReuseIdentifier];
}

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];

    // Restore search mechanism (if enabled)
    ignoreSearchRequest = NO;
}

- (void)viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];

    // The user may still press search button whereas the view disappears
    ignoreSearchRequest = YES;
}


#pragma mark - Override MXKViewController

- (void)onKeyboardShowAnimationComplete
{
    // Report the keyboard view in order to track keyboard frame changes
    self.keyboardView = _searchSearchBar.inputAccessoryView.superview;
}

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated"
- (void)setKeyboardHeight:(CGFloat)keyboardHeight
{
    // Deduce the bottom constraint for the table view (Don't forget the potential tabBar)
    CGFloat tableViewBottomConst = keyboardHeight - self.bottomLayoutGuide.length;
    // Check whether the keyboard is over the tabBar
    if (tableViewBottomConst < 0)
    {
        tableViewBottomConst = 0;
    }

    // Update constraints
    _searchTableViewBottomConstraint.constant = tableViewBottomConst;

    // Force layout immediately to take into account new constraint
    [self.view layoutIfNeeded];
}
#pragma clang diagnostic pop

- (void)destroy
{
    _searchTableView.dataSource = nil;
    _searchTableView.delegate = nil;
    _searchTableView = nil;

    dataSource.delegate = nil;
    [dataSource destroy];
    dataSource = nil;
    
    [super destroy];
}

#pragma mark -

- (void)displaySearch:(MXKSearchDataSource*)searchDataSource
{
    // Cancel registration on existing dataSource if any
    if (dataSource)
    {
        dataSource.delegate = nil;
        
        // Remove associated matrix sessions
        [self removeMatrixSession:dataSource.mxSession];
        
        [dataSource destroy];
    }

    dataSource = searchDataSource;
    dataSource.delegate = self;
    
    // Report the related matrix sessions at view controller level to update UI according to sessions state
    [self addMatrixSession:searchDataSource.mxSession];

    if (_searchTableView)
    {
        // Set up table data source
        _searchTableView.dataSource = dataSource;
    }
}


#pragma mark - UIBarButton handling

- (void)setEnableBarButtonSearch:(BOOL)enableBarButtonSearch
{
    _enableBarButtonSearch = enableBarButtonSearch;
    [self refreshUIBarButtons];
}

- (void)refreshUIBarButtons
{
    if (_enableBarButtonSearch)
    {
        self.navigationItem.rightBarButtonItems = @[searchBarButton];
    }
    else
    {
        self.navigationItem.rightBarButtonItems = nil;
    }
}

#pragma mark - MXKDataSourceDelegate

- (Class<MXKCellRendering>)cellViewClassForCellData:(MXKCellData*)cellData
{
    return MXKSearchTableViewCell.class;
}

- (NSString *)cellReuseIdentifierForCellData:(MXKCellData*)cellData
{
    return MXKSearchTableViewCell.defaultReuseIdentifier;
}

- (void)dataSource:(MXKDataSource *)dataSource didCellChange:(id)changes
{
    __block CGPoint tableViewOffset;
    
    if (!shouldScrollToBottomOnRefresh)
    {
        // Store current tableview scrolling point to restore it after [UITableView reloadData]
        // This avoids unexpected scrolling for the user
        tableViewOffset = _searchTableView.contentOffset;
    }

    [_searchTableView reloadData];

    if (shouldScrollToBottomOnRefresh)
    {
        [self scrollToBottomAnimated:NO];
        shouldScrollToBottomOnRefresh = NO;
    }
    else
    {
        // Restore the user scrolling point by computing the offset introduced by new cells
        // New cells are always introduced at the top of the table
        NSIndexSet *insertedIndexes = (NSIndexSet*)changes;

        // Get each new cell height
        [insertedIndexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL * _Nonnull stop) {

            MXKCellData* cellData = [self.dataSource cellDataAtIndex:idx];
            Class<MXKCellRendering> class = [self cellViewClassForCellData:cellData];

            tableViewOffset.y += [class heightForCellData:cellData withMaximumWidth:self->_searchTableView.frame.size.width];

        }];

        [_searchTableView setContentOffset:tableViewOffset animated:NO];
    }

    self.title = [NSString stringWithFormat:@"%@ (%tu)", self.dataSource.searchText, self.dataSource.serverCount];
}

- (void)dataSource:(MXKDataSource*)dataSource2 didStateChange:(MXKDataSourceState)state
{
    // MXKSearchDataSource comes back to the `MXKDataSourceStatePreparing` when searching
    if (state == MXKDataSourceStatePreparing)
    {
        _noResultsLabel.hidden = YES;
        [self startActivityIndicator];
    }
    else
    {
        [self stopActivityIndicator];

        // Display "No Results" if a search is active with an empty result
        if (dataSource.searchText.length && ![dataSource tableView:_searchTableView numberOfRowsInSection:0])
        {
            _noResultsLabel.hidden = NO;
            _searchTableView.hidden = YES;
        }
        else
        {
            _noResultsLabel.hidden = YES;
            _searchTableView.hidden = NO;
        }
    }
}

#pragma mark - UITableView delegate

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    MXKCellData *cellData = [dataSource cellDataAtIndex:indexPath.row];

    Class<MXKCellRendering> class = [self cellViewClassForCellData:cellData];
    return [class heightForCellData:cellData withMaximumWidth:tableView.frame.size.width];
}


- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    // Must be implemented at app level
}

- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath*)indexPath
{
    // Release here resources, and restore reusable cells
    if ([cell respondsToSelector:@selector(didEndDisplay)])
    {
        [(id<MXKCellRendering>)cell didEndDisplay];
    }
}

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset
{
    // Detect vertical bounce at the top of the tableview to trigger pagination
    if (scrollView == _searchTableView)
    {
        // paginate ?
        if (scrollView.contentOffset.y < -64)
        {
            [self triggerBackPagination];
        }
    }
}

#pragma mark - UISearchBarDelegate

- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar
{
    // "Done" key has been pressed
    [searchBar resignFirstResponder];

    // Apply filter
    if (searchBar.text.length)
    {
        shouldScrollToBottomOnRefresh = YES;
        [dataSource searchMessages:searchBar.text force:NO];
    }
}

- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar
{
    // Leave search
    [searchBar resignFirstResponder];

    self.searchSearchBar.hidden = YES;
    self.searchSearchBarHeightConstraint.constant = 0;
    [self.view setNeedsUpdateConstraints];

    self.searchSearchBar.text = nil;
}

#pragma mark - Actions

- (void)showSearchBar:(id)sender
{
    // The user may have pressed search button whereas the view controller was disappearing
    if (ignoreSearchRequest)
    {
        return;
    }

    if (self.searchSearchBar.isHidden)
    {
        self.searchSearchBar.hidden = NO;
        self.searchSearchBarHeightConstraint.constant = 44;
        [self.view setNeedsUpdateConstraints];

        [self.searchSearchBar becomeFirstResponder];
    }
    else
    {
        [self searchBarCancelButtonClicked: self.searchSearchBar];
    }
}

#pragma mark - Private methods

- (void)triggerBackPagination
{
    // Paginate only if possible
    if (NO == dataSource.canPaginate)
    {
        return;
    }

    [dataSource paginateBack];
}

- (void)scrollToBottomAnimated:(BOOL)animated
{
    if (_searchTableView.contentSize.height)
    {
        CGFloat visibleHeight = _searchTableView.frame.size.height - _searchTableView.adjustedContentInset.top - _searchTableView.adjustedContentInset.bottom;
        if (visibleHeight < _searchTableView.contentSize.height)
        {
            CGFloat wantedOffsetY = _searchTableView.contentSize.height - visibleHeight - _searchTableView.adjustedContentInset.top;
            CGFloat currentOffsetY = _searchTableView.contentOffset.y;
            if (wantedOffsetY != currentOffsetY)
            {
                [_searchTableView setContentOffset:CGPointMake(0, wantedOffsetY) animated:animated];
            }
        }
        else
        {
            _searchTableView.contentOffset = CGPointMake(0, - _searchTableView.adjustedContentInset.top);
        }
    }
}

@end
